package parser_protobuf3

import (
	"fmt"
	"path/filepath"
	"strings"

	"gitee.com/woodpile/wgs.ths.code.generate/definition"
	"github.com/jhump/protoreflect/desc" //cspell: disable-line
	"google.golang.org/protobuf/reflect/protoreflect"
)

func (p *Parser) parseMessages(ctx *definition.Context) error {
	fParseFile := func(dir, filename string) error {
		if definition.GlobalConfig.Verbose {
			fmt.Println("ParseFile ", filepath.Join(dir, filename))
		}

		df := p.getOrNewDFFile(ctx, dir, filename)
		defer ctx.AddDefinedFile(df)

		arrProtoFile, err := protoParser.ParseFiles(df.Path)
		if err != nil {
			return fmt.Errorf("proto parse file %s failed: %w", df.Path, err)
		}
		for _, pf := range arrProtoFile {
			if err := p.parseProtoMessages(ctx, pf, df); err != nil {
				return fmt.Errorf("parse file %s content failed: %w", df.Path, err)
			}
		}
		return nil
	}
	for _, dir := range definition.GlobalConfig.Parser.Dir {
		if err := definition.ForeachFileRecursively(dir, p.filterFile, fParseFile); err != nil {
			return err
		}
	}
	return nil
}

func (p *Parser) parseProtoMessages(ctx *definition.Context, pf *desc.FileDescriptor, df *definition.DFFile) error {
	if !pf.IsProto3() {
		return fmt.Errorf("only support proto3")
	}

	// fmt.Printf("=> %s : %+v\n", pf.GetName(), pf)

	for _, pm := range pf.GetMessageTypes() {
		ds, err := p.parseProtoMessage(pm)
		if err != nil {
			return fmt.Errorf("parse message %s failed: %w", pm.GetName(), err)
		}
		ds.SourceFile = df.Path
		df.Structs = append(df.Structs, ds)
	}

	return nil
}

func (p *Parser) parseProtoMessage(pm *desc.MessageDescriptor) (*definition.DFStruct, error) {
	ds := &definition.DFStruct{}

	// fmt.Printf("=> %+v\n", pm)

	if si := pm.GetSourceInfo(); si != nil {
		ds.Comment = strings.TrimSpace(si.GetLeadingComments())
	}

	if mo := pm.GetMessageOptions(); mo != nil {
		options := p.parseProtoOptionUsing(mo.String())
		ds.Options = make(map[string]string, len(options))
		for k, v := range options {
			op, exist := p.ProtoOptions.messageAsNum[k]
			if !exist {
				return nil, fmt.Errorf("invalid message option: %s", k)
			}
			ds.Options[op.Name] = v
		}
	}

	for _, po := range pm.GetOneOfs() {
		do := &definition.DFStructOneOf{}
		do.Name = po.GetName()
		ds.OneOfs = append(ds.OneOfs, do)

		if si := po.GetSourceInfo(); si != nil {
			if si.GetLeadingComments() != "" {
				do.Comment = strings.TrimSpace(si.GetLeadingComments())
			}
			if si.GetTrailingComments() != "" {
				if do.Comment != "" {
					do.Comment += "\n"
				}
				do.Comment += strings.TrimSpace(si.GetTrailingComments())
			}
		}

		if doo := po.GetOneOfOptions(); doo != nil {
			options := p.parseProtoOptionUsing(doo.String())
			do.Options = make(map[string]string, len(options))
			for k, v := range options {
				op, exist := p.ProtoOptions.oneOfAsNum[k]
				if !exist {
					return nil, fmt.Errorf("invalid oneOf option: %s", k)
				}
				do.Options[op.Name] = v
			}
		}
	}

	ds.Name = pm.GetName()
	for _, pf := range pm.GetFields() {
		df, err := p.parseProtoMessageField(pf)
		if err != nil {
			return nil, fmt.Errorf("parse field %s failed: %w", pf.GetName(), err)
		}
		if pf.GetOneOf() != nil {
			do := ds.GetOneOf(pf.GetOneOf().GetName())
			do.Fields = append(do.Fields, df)
		} else {
			ds.Fields = append(ds.Fields, df)
		}
	}

	return ds, nil
}

func (p *Parser) parseProtoMessageField(pf *desc.FieldDescriptor) (*definition.DFStructField, error) {
	df := &definition.DFStructField{}

	df.Name = pf.GetName()

	// fmt.Printf("-----%+v\n", pf)

	if si := pf.GetSourceInfo(); si != nil {
		if si.GetLeadingComments() != "" {
			df.Comment = strings.TrimSpace(si.GetLeadingComments())
		}
		if si.GetTrailingComments() != "" {
			if df.Comment != "" {
				df.Comment += "\n"
			}
			df.Comment += strings.TrimSpace(si.GetTrailingComments())
		}
	}

	if fo := pf.GetFieldOptions(); fo != nil {
		// fmt.Printf("=====%+v\n", fo)
		options := p.parseProtoOptionUsing(fo.String())
		df.Options = make(map[string]string, len(options))
		for k, v := range options {
			op, exist := p.ProtoOptions.fieldAsNum[k]
			if !exist {
				return nil, fmt.Errorf("invalid option in field [%s]: %s", df.Name, k)
			}
			df.Options[op.Name] = v
		}
	}

	if pf.IsRepeated() {
		df.FieldType.IsArray = true
	}
	if pf.IsMap() {
		df.FieldType.IsMap = true
		df.FieldType.IsArray = false

		kf := pf.GetMapKeyType()
		if kf.GetEnumType() != nil {
			df.FieldType.KeyType = &definition.DFType{
				IsEnum: true,
				Source: kf.GetEnumType().GetName(),
			}
		} else {
			sourceTypeStr, err := p.parseFieldSimpleType(kf)
			if err != nil {
				return nil, fmt.Errorf("parse map field key type failed: %w", err)
			}
			df.FieldType.KeyType = &definition.DFType{
				Source: sourceTypeStr,
			}
		}

		pf = pf.GetMapValueType()
	}
	if pf.GetEnumType() != nil {
		df.FieldType.ValueType.IsEnum = true
		df.FieldType.ValueType.Source = pf.GetEnumType().GetName()
	} else if pf.GetMessageType() != nil {
		df.FieldType.ValueType.IsStruct = true
		df.FieldType.ValueType.Source = pf.GetMessageType().GetName()
	} else {
		sourceTypeStr, err := p.parseFieldSimpleType(pf)
		if err != nil {
			return nil, fmt.Errorf("parse field type failed: %w", err)
		}
		df.FieldType.ValueType.Source = sourceTypeStr
	}

	return df, nil
}

func (p *Parser) parseFieldSimpleType(pf *desc.FieldDescriptor) (string, error) {
	sourceTypeStr := "invalid"
	switch protoreflect.Kind(pf.GetType().Number()) {
	case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
		sourceTypeStr = "int32"
	case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
		sourceTypeStr = "int64"
	case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
		sourceTypeStr = "uint32"
	case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
		sourceTypeStr = "uint64"
	case protoreflect.FloatKind:
		sourceTypeStr = "float32"
	case protoreflect.DoubleKind:
		sourceTypeStr = "float64"
	case protoreflect.BoolKind:
		sourceTypeStr = "bool"
	case protoreflect.StringKind:
		sourceTypeStr = "string"
	case protoreflect.BytesKind:
		sourceTypeStr = "bytes"
	default:
		return "", fmt.Errorf("invalid field type: %s", pf.GetType().String())
	}
	return sourceTypeStr, nil
}
